# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Basic game dialogs, entries and progress for tile/tk.
"""
__docformat__ = 'restructuredtext en'
#_ = lambda x: x

from tkinter import Toplevel, DoubleVar
import tkinter.ttk as ttk
import tkinter.simpledialog
from collections import namedtuple

from ..utillib.cache import IMAGE_CACHE
from . import constants as vcnst
from .widget import ScrolledTextFrame

padding = namedtuple('Pad', ('x', 'y'))

PAD = padding(x=5, y=5)


class MessageDialog(tkinter.simpledialog.Dialog, object):
    """
    Message Dialog that set a message with an icon or image in compound label.
    The dialog also can timeout and destroy itself after a timeout period. Any
    additional label keywords may be passed and applied to the label.
    """
    _kwargs = {'bitmap': 'info',
               'compound': vcnst.LEFT,
               'wraplength': 240,
               'style': 'npsDialog.TLabel'}
    state = True

    def __init__(self, parent, title, text, **kwargs):
        """
        __init__(parent:Widget, title:str, text:str[, timeout:int=0,
          bitmap:str='info', **ttkLabel keywords**]) -> None

        Init a message dialog by setting text and keywords for use by other
        methods. Calls parent **Dialog** method.
        """
        self._kwargs.update(kwargs)
        self._kwargs['text'] = text
        super(MessageDialog, self).__init__(parent, title)

    def body(self, parent):
        """
        body(parent:Widget) -> Widget

        Sets after callback to destroy dialog if timeout keyword. Then, sets
        image as a named bitmap keyword or image keyword with an image placed
        instead of a bitmap. Default image will be an info bitmap. Finally
        image and text placed in a compound label.
        """
        kwargs = self._kwargs.copy()

        # Dialog timeout?
        if 'timeout' in kwargs:
            if kwargs['timeout'] > 0:
                self.after(kwargs['timeout'], self.time_out)
            del kwargs['timeout']

        # Dialog image?
        if not kwargs.get('image'):
            kwargs['image'] = IMAGE_CACHE.load_icon(kwargs['bitmap'])
        del kwargs['bitmap']

        widget = ttk.Label(parent, **kwargs)
        widget.pack(fill=vcnst.BOTH, expand=True, padx=PAD.x, pady=PAD.y)

    def buttonbox(self):
        """
        buttonbox() -> None

        Overide standard button box with a single buttons for acknowldgement.
        """
        box = ttk.Frame(self)

        widget = ttk.Button(box, text=_('Ok'),  style='npsDialog.TButton',
                            command=self.ok, default=vcnst.ACTIVE)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)

        self.bind('<Escape>', self.ok)
        self.bind('<Return>', self.ok)
        box.pack()

    def time_out(self, event=None):
        """
        time_out(event:Event=None) -> None

        Handle a timeout event by setting state False and then reseting the
        focus on the parent before calling destroy.
        """
        self.state = False
        if self.parent is not None:
            self.parent.focus_set()
        self.destroy()

    def apply(self):
        """
        apply() -> None

        Process an acknowledgement.
        """
        self.result = True


class ConfirmDialog(MessageDialog):
    """
    Confirmation Dialog that set a message with an icon or image in compound
    label. The dialog also can timeout and destroy itself after a timeout
    period. Any additional label keywords may be passed and applied to the
    label.
    """
    _kwargs = {'bitmap': 'question',
               'compound': vcnst.LEFT,
               'wraplength': 240,
               'style': 'npsDialog.TLabel'}

    def buttonbox(self):
        """
        buttonbox() -> None

        Overide standard button box with buttons for making a decision about the
        message.
        """
        box = ttk.Frame(self)

        widget = ttk.Button(box, text=_('Yes'), style='npsDialog.TButton',
                            command=self.ok, default=vcnst.ACTIVE)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)
        widget = ttk.Button(box, text=_('No'),  style='npsDialog.TButton',
                            command=self.cancel)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)

        self.bind('<Escape>', self.cancel)
        self.bind('<Return>', self.ok)
        box.pack()


class DoneDialog(MessageDialog):
    """
    Game complete dialog that set a message with an icon or image in compound
    label. The dialog also can timeout and destroy itself after a timeout
    period. Any additional label keywords may be passed and applied to the
    label.
    """

    def buttonbox(self):
        """
        buttonbox() -> None

        Overide standard button box with buttons for decision on whether to play
        the same or new game. Also add a button to not make a decision.
        """
        box = ttk.Frame(self)

        widget = ttk.Button(box, text=_('New game'), style='npsDialog.TButton',
                            command=self.ok, default=vcnst.ACTIVE)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)

        widget = ttk.Button(box, text=_('Restart game'),
                            style='npsDialiog.TButton', command=self.restart)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)

        widget = ttk.Button(box, text=_('Cancel'), style='npsDialog.TButton',
                            command=self.cancel)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)

        self.bind('<Return>', self.ok)
        self.bind('<Escape>', self.cancel)

        box.pack()

    def restart(self, event=None):
        """
        restart(event:Event=None) -> None

        Handle the restart decision setting result False and then reseting the
        focus on the parent before calling destroy.
        """
        self.result = 0x20
        if self.parent is not None:
            self.parent.focus_set()
        self.destroy()

    def apply(self):
        """
        apply() -> None

        Process an acknowledgement.
        """
        self.result = 0x10


class ProgressBar(Toplevel, object):
    """
    Progress dialog that shows completion of intializing.
    """
    _sum = 0

    def __init__(self, parent, title=None):
        """
        __init__(parent:Widget, title:str) -> None

        Init the progress dialog by calling the parent Toplevel method and
        and setting no-resize transient dialog that does not halt the parent.
        """
        super(ProgressBar, self).__init__(parent)

        parent.withdraw()  # hide game during intitialize
        self.withdraw()    # hide self until dialog setup
        self.wm_overrideredirect(True)
        self.wm_group(None)
        self.wm_resizable(False, False)
        self.config(cursor='watch')

        if title:
            self.title(title)

        self._label = title

        self.parent = parent

        self._completed = DoubleVar()
        self._completed.set(0)

        body = ttk.Frame(self)
        self.body(body)
        body.pack(padx=5, pady=5)

        self.protocol("WM_DELETE_WINDOW", self.destroy)

        if self.parent is not None:
            self.geometry("+%d+%d" % (parent.winfo_rootx() + 50,
                                      parent.winfo_rooty() + 50))

        self.deiconify()  # become visibile now
        self.focus_set()

        # wait for window to appear on screen before calling grab_set
        self.wait_visibility()
        self.grab_set()

    def body(self, master):
        """
        body(master:Widget) -> Widget

        Sets an image either side of a progress bar to track parent initialize
        progress.
        """
        raise NotImplementedError

    def destroy(self):
        """
        destroy() -> None

        Destroy the dialog.
        """
        if self.parent is not None:
            self.parent.deiconify()
            self.parent.focus_set()
        super(ProgressBar, self).destroy()

    @property
    def percent(self):
        """
        precent -> float

        Returns current percent complete.
        """
        return self._completed.get()

    @percent.setter
    def percent(self, value):
        """
        precent = float|int

        Sets current percent complete.
        """
        if isinstance(value, (float, int)):
            self._completed.set(value)

    def reset(self, percent=0):
        """
        reset(percent:float|int=0)

        Sets current percent complete.
        """
        self.percent = percent

    def update(self, percent=None, step=1):
        """
        update(percent:float|int=None, step:int=1) -> None

        Update progress toward completion.
        """
        self._sum += step
        if percent is None:
            self.percent += step
        elif percent > self.percent:
            self.percent = percent
        else:
            return
        self.percent = min(100, max(0, self.percent))
        super(ProgressBar, self).update()


class _EntryDialog(tkinter.simpledialog.Dialog, object):
    """
    Abstract entry dialog that set a message with an entry below of value.
    """
    _kwargs = {'takefocus': False}

    def __init__(self, parent, title, label=None, value=None):
        """
        __init__(parent:Widget, title:str[, label:str=None, value:str=None])
          -> None

        Init the entry dialog by setting the label and value then calling parent
        Dialog method with title.
        """
        self._label = label
        self._value = value
        super(_EntryDialog, self).__init__(parent, title)

    def body(self, parent):
        """
        body(parent:Widget) -> Widget

        Sets label with label keyword value and entry with value keyword value.
        Either side of the entry a button is placed to decrease or increase
        entry value.
        """
        raise NotImplementedError

    def buttonbox(self):
        """
        buttonbox() -> None

        Overide standard button box with buttons for dialog acknowledgement when
        mode is not question. In question mode, show buttons for a decision
        about message to agree or disagree.
        """
        box = ttk.Frame(self)
        widget = ttk.Button(box, text=_('Ok'), style='npsDialog.TButton',
                            command=self.ok, default=vcnst.ACTIVE)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)

        widget = ttk.Button(box, text=_('Cancel'), style='npsDialog.TButton',
                            command=self.cancel)
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)

        self.bind('<Return>', self.ok)
        self.bind('<Escape>', self.cancel)

        box.pack()


class DigitEntry(_EntryDialog):
    """
    Digit entry dialog that set a message with an entry below of value. Either
    side of the entry are buttons to decrease or increase the entry value by 1.
    """
    _entry = None

    def body(self, master):
        """
        body(master:Widget) -> Widget

        Sets label with label keyword value and entry with value keyword value.
        Either side of the entry a button is placed to decrease or increase
        entry value.
        """
        if self._label:
            widget = ttk.Label(master, text=self._label, **self._kwargs)
            widget.pack(padx=PAD.x, pady=PAD.y)

        box = ttk.Frame(master)
        widget = ttk.Button(box, text='-', width=3,
                            command=lambda v=-1: self.adjust(v))
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)
        entry = ttk.Entry(box, exportselection=False)
        entry.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)
        widget = ttk.Button(box, text='+', width=3,
                            command=lambda v=+1: self.adjust(v))
        widget.pack(side=vcnst.LEFT, padx=PAD.x, pady=PAD.y)
        box.pack()

        entry.insert(0, self._value)
        self._entry = entry
        return self._entry

    def adjust(self, value):
        """
        adjust(self, value) -> None

        Adjust entry value by passed value.
        """
        self.validate()
        result = self._entry.get()
        self._entry.delete(0, len(result))
        self._entry.insert(0, int(result) + value)

    def validate(self):
        """
        validate() -> bool

        Check entry value has only digits, if not reset value to passed.
        """
        result = self._entry.get()
        if not result.isdigit():
            self._entry.delete(0, len(result))
            self._entry.insert(0, self._value)
            return False
        return True

    def apply(self):
        """
        apply() -> None

        Set result with entry value.
        """
        self.result = int(self._entry.get())


class TextEntry(_EntryDialog):
    """
    Text entry dialog that set a message with an entry below of value.
    """
    _entry = None

    def body(self, master):
        """
        body(master:Widget) -> Widget

        Sets label with label keyword value and entry with value keyword value.
        Add vertical scrollbar to entry on right.
        """
        if self._label:
            widget = ttk.Label(master, text=self._label, **self._kwargs)
            widget.pack(padx=PAD.x, pady=PAD.y)

        self._entry = ScrolledTextFrame(master)

        if self._value:
            old_state = entry['state']
            entry.config(state='normal')
            entry.insert('insert', self._value)
            entry.config(state=old_state)

    def apply(self):
        """
        apply() -> None

        Set result with entry value.
        """
        self.result = self._entry.get('1.0', 'end')